#-*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

__all__ = ['DataConverterHandler']

from StringIO import StringIO

import networkx

from pyLot.core import mkdtempu, Path, Handler
from pyLot.application import APPLICATION
from pyLot.recording import recordable

# Not yet used.
WEIGHT_COMPILED_ADAPTER = 1
WEIGHT_COMPILED_READER = 2
WEIGHT_COMPILED_WRITER = 3

WEIGHT_PYTHON_ADAPTER = 10
WEIGHT_PYTHON_READER = 20
WEIGHT_PYTHON_WRITER = 30

WEIGHT_CONVERTER = 4
WEIGHT_FORMAT = 50

def make_id(obj):
  try :
    identifier = obj.identifier
  except AttributeError :
    identifier = str(obj)
  return identifier



class DataConverterHandler(Handler):

  def __init__(self):
    Handler.__init__(self)

    self.attrs_reprs = dict(shape='box')
    self.attrs_fmts = dict(color='red', shape='folder')

    self._createGraph()
    self._recorded = False
    try :
      networkx.write_dot(self.graph, 'graph.dot')
    except :
      pass
#    self.displayGraph()

  pm = property(fget=lambda self:APPLICATION.getPluginManager())

  def getPaths(self, sources, targets):
    paths = []
    for source in sources :
      for target in targets :
        try :
          path = networkx.shortest_path(self.graph, source=source, target=target)
        except networkx.NetworkXNoPath :
          pass
        else :
          paths.append(path)
    return paths

  def getPathInformations(self, opt_in, opt_out):
    all_paths = []

    if opt_in in self.fmts and opt_out in self.fmts :
      if opt_in == opt_out :
        # Try to use a reader and a writer to come back to initial format
        # This allows to check and clean input data
        paths_in = self.getPaths([opt_in], self.readers.keys())
        last_path = []
        if paths_in :
          for path_in in paths_in :
            reader = self.readers[path_in[-1]]
            repr_id = make_id(reader.datarepr)
            paths_out = self.getPaths([repr_id], [opt_out])
            if paths_out :
              path = tuple(path_in + paths_out[0])
              if len(path) < len(last_path) or not last_path:
                last_path = path
        if last_path :
          all_paths.append(last_path)
        if not all_paths :
          path_in = self.getPaths([opt_in], self.converters.keys())
          if path_in :
            converter = path_in[0][-1]
            path_out = self.getPaths([converter], [opt_out])
            if path_out :
              all_paths.append(tuple(path_in[0] + path_out[0]))
      else :
        all_paths = self.getPaths([opt_in], [opt_out])
    else :
      all_paths = self.getPaths([opt_in], [opt_out])

    options = []
    modes = []

    for path in all_paths :
      _options = []
      _modes = dict(input=set([]), output=set([]))
      for node in path :
        if node in self.writers or node in self.adapters or node in self.readers :
          try :
            opt = self.nodes[node].options
          except AttributeError :
            pass
          else :
            if opt :
              _options.append((node, opt))

      if path[1] in self.readers :
        _modes['input'].update(set(['data', 'strlist', 'python', 'path']))
      if path[1] in self.converters :
        _modes['input'].add('path')
      if path[-1] in self.reprs :
        _modes['output'].add('data')
      if path[-2] in self.writers :
        _modes['output'].update(set(['data', 'iterator', 'path']))
      if path[-2] in self.converters :
        _modes['output'].add('path')
      modes.append(_modes)
      options.append(_options)

    return all_paths, modes, options

  def getConverterInputFormats(self, mode='external'):
    fmt_list = []

    if mode == 'external' :
      keys = self.readers.keys() + self.converters.keys()
    elif mode == 'python' :
      keys = self.readers.keys()
    elif mode == 'format' :
      keys = self.readers.keys()
    else :
      keys = []

    for obj in keys :
      fmts = self.graph.predecessors(obj)
      fmt_list += fmts
    return fmt_list

  def getCreatorOutputFormats(self, mode='external'):
    fmt_list = []

    if mode == 'external' :
      keys = self.writers.keys() + self.converters.keys()
    elif mode == 'python' :
      keys = self.writers.keys()
    elif mode == 'format' :
      keys = self.writers.keys()
    else :
      keys = []

    for obj in keys :
      fmts = self.graph.successors(obj)
      fmt_list += fmts
    return fmt_list

  def getCreatorInputFormats(self, target_fmt):
    fmts = []
    for fmt in self.fmts :
      path = self.getConverterPaths(fmt, target_fmt)
      if path :
        fmts.append(path[0][0])
    return fmts

  def getConverterOutputFormats(self, source_fmt, fmts=None):
    u"""
    if fmts is None, return all output formats. Else limit it to fmts list
    """
    output_fmts = []
    if fmts is None :
      fmts = self.fmts

    for fmt in fmts :
      path = self.getConverterPaths(source_fmt, fmt)
      if path :
        output_fmts.append(path[0][-1])
    return output_fmts

  def getConverterPaths(self, opt_in, opt_out):
    paths, modes, options = self.getPathInformations(opt_in, opt_out)
    return paths

  def getConverterModes(self, opt_in, opt_out):
    paths, modes, options = self.getPathInformations(opt_in, opt_out)
    return modes

  def getOptionAlongPath(self, opt_in, opt_out):
    paths, modes, options = self.getPathInformations(opt_in, opt_out)
    if options :
      return options[0]
    else :
      return []

  def createData(self, datarepr_id):
    if datarepr_id in self.reprs :
      return self.reprs[datarepr_id].new()
    else :
      return None

  def toStream(self, stream):
    if isinstance(stream, (list, tuple)) :
      new_stream = StringIO()
      new_stream.write('\n'.join(stream))
      new_stream.pos = 0
    else :
      new_stream = stream
    return new_stream

  def convert(self, fmt_in, fmt_out, input_data, input_mode,
                    output_mode, output_path=None):
    """
    only stream, python and path are supported
    """
    return self.convertData(fmt_in, fmt_out, input_data, input_mode,
                        output_mode, output_path)

  def convertData(self, fmt_in, fmt_out, input_data, input_mode,
                    output_mode, output_path=None):

    write_tmp_files = False

    path = self.getConverterPaths(fmt_in, fmt_out)[0]

    for obj_id in path[1:-1] :
      if obj_id in self.fmts :
        write_tmp_files = True

    if write_tmp_files :
      tmpdir = mkdtempu()

    path_out = path[-1]

    begin = 0

    if input_mode in ('strlist', 'data', 'python', 'stream') :
      if fmt_in in self.reprs :
        begin = 1
      elif fmt_in in self.fmts :
        begin = 2
      else:
        raise ValueError, '%s is not recognized'

    if input_mode == 'strlist' :
      stream = self.toStream(input_data)
      data = self.readers[path[1]].read(stream)
      data_mode = 'data'
    elif input_mode == 'stream' :
      data = self.readers[path[1]].read(input_data)
      data_mode = 'data'
    elif input_mode == 'data' :
      data = input_data
      data_mode = 'data'
    elif input_mode == 'python' :
      pycode = input_data
      datarepr = self.readers[path[1]].datarepr
      data = datarepr.new()
      dic = {'data':data, 'DataRepr':datarepr.__class__}
      exec pycode in dic
      try :
        data = dic['data']
      except KeyError :
        pass
      data_mode = 'data'
    elif input_mode == 'path' :
      data = input_data
    else :
      begin = 0

    for i in range(begin, len(path)) :
      obj_id = path[i]

      if obj_id in self.fmts :
        data_mode = 'path'

      if obj_id in self.readers :
        if data_mode == 'path':
          data = self.readers[obj_id].open(data)
          data_mode = 'data'
        else :
          data = self.readers[obj_id].read(data)
          data_mode = 'data'

      if obj_id in self.reprs :
        data_mode = 'data'
        try :
          end = path[i + 2]
        except IndexError :
          pass
        else :
          if end == path_out and output_mode == 'data':
            break

      if obj_id in self.adapters :
        data = self.adapters[obj_id].convert(data)
        data_mode = 'data'

      if obj_id in self.writers :
        if path[i + 1] == path_out and output_mode == 'path' :
          data = self.writers[obj_id].save(data, output_path)
          data = output_path
          data_mode = 'path'
        elif path[i + 1] == path_out :
          data = self.writers[obj_id].stream(data)
          data_mode = 'data'
        elif path[i + 2] in self.converters :
          tmp_path = Path(tmpdir, u'step_%02d' % i)
          self.writers[obj_id].save(data, tmp_path)
          data = tmp_path
          data_mode = 'path'
        else :
          data = self.writers[obj_id].stream(data)
          data_mode = 'data'

      if obj_id in self.converters :
        fmt_in = self.fmts[path[i - 1]]
        fmt_out = self.fmts[path[i + 1]]
        if path[i + 1] == path_out:
          cg = self.converters[obj_id].cmdGroup(fmt_in, data, fmt_out, output_path)
          data = output_path
          data_mode = 'path'
        else :
          tmp_path = Path(tmpdir, 'step_%02d' % i)
          cg = self.converters[obj_id].cmdGroup(fmt_in, data, fmt_out, tmp_path)
          data = tmp_path
          data_mode = 'path'
        raise NotImplementedError
        #APPLICATION.process.run(cg)

    if write_tmp_files :
      tmpdir.rmtree()

    return data

  def read(self, fmt_in, repr_out, input_data, input_mode):

    paths = self.getPaths([fmt_in], [repr_out])
    if not paths :
      return
    path = paths[0]

    write_tmp_files = False
    for obj_id in path[1:-1] :
      if obj_id in self.fmts :
        write_tmp_files = True

    if write_tmp_files :
      tmpdir = mkdtempu()

    path_out = path[-1]

    begin = 0
    if input_mode == 'strlist' :
      stream = self.toStream(input_data)
      data = self.readers[path[1]].read(stream)
      data_mode = 'data'
      begin = 2
    elif input_mode == 'stream' :
      data = self.readers[path[1]].read(input_data)
      data_mode = 'data'
      begin = 2
    elif input_mode == 'data' :
      data = input_data
      data_mode = 'data'
      begin = 2
    elif input_mode == 'python' :
      pycode = input_data
      datarepr = self.readers[path[1]].datarepr
      data = datarepr.new()
      dic = {'data':data, 'DataRepr':datarepr.__class__}
      exec pycode in dic
      try :
        data = dic['data']
      except KeyError :
        pass
      data_mode = 'data'
      begin = 2
    elif input_mode == 'path' :
      data = input_data
    else :
      begin = 0



    for i in range(begin, len(path)) :
      obj_id = path[i]

      if obj_id in self.fmts :
        data_mode = 'path'

      if obj_id in self.readers :
        if data_mode == 'path':
          data = self.readers[obj_id].open(data)
          data_mode = 'data'
        else :
          data = self.readers[obj_id].read(data)
          data_mode = 'data'

      if obj_id in self.reprs :
        data_mode = 'data'

      if obj_id in self.adapters :
        data = self.adapters[obj_id].convert(data)
        data_mode = 'data'

      if obj_id in self.writers :
        if path[i + 1] == path_out :
          data = self.writers[obj_id].stream(data)
          data_mode = 'data'
        elif path[i + 2] in self.converters :
          tmp_path = Path(tmpdir, u'step_%02d' % i)
          self.writers[obj_id].save(data, tmp_path)
          data = tmp_path
          data_mode = 'path'
        else :
          data = self.writers[obj_id].stream(data)
          data_mode = 'data'

      if obj_id in self.converters :
        fmt_in = self.fmts[path[i - 1]]
        fmt_out = self.fmts[path[i + 1]]
        tmp_path = Path(tmpdir, 'step_%02d' % i)
        self.converters[obj_id].convert(fmt_in, data, fmt_out, tmp_path)
        data = tmp_path
        data_mode = 'path'

    if write_tmp_files :
      tmpdir.rmtree()

    return data

  def _getPathLabels(self):
    labels = {}
    for node in self.nodes :
      parts = node.split('.')[1:]
      parts[0] = parts[0].replace('format', 'fmt')
      parts[0] = parts[0][0:4]
      labels[node] = '.'.join(parts)
    return labels

  def getPathLabel(self, path):

    txt = 'Chemin (d=%02d) :' % len(path)
    labels = self._getPathLabels()
    path = [labels[node] for node in path]
    txt += ' -> '.join(path)
    return txt

  def _createGraph(self):

    self.graph = networkx.DiGraph()

    self.fmts = {}
    self.readers = {}
    self.reprs = {}
    self.writers = {}
    self.adapters = {}
    self.converters = {}

    for obj_class in self.pm.get_extensions('dataconverter.reprs'):
      obj = obj_class()
      obj_id = obj.identifier
      self.reprs[obj_id] = obj
      self.graph.add_node(obj_id, self.attrs_reprs)


    for obj_class in self.pm.get_extensions('dataconverter.formats'):
      obj = obj_class()
      obj_id = obj.identifier
      self.fmts[obj_id] = obj
      self.graph.add_node(obj_id, self.attrs_fmts)

    for obj_class in self.pm.get_extensions('dataconverter.readers'):
      try :
        dataformat = self.fmts[obj_class.dataformat_id]
        datarepr = self.reprs[obj_class.datarepr_id]
      except KeyError :
        print 'Pb loading %s' % obj_class
      else :
        obj = obj_class(dataformat, datarepr)
        obj_id = obj.identifier
        self.readers[obj_id] = obj
        self.graph.add_node(obj_id)
        self.graph.add_edge(dataformat.identifier, obj_id)
        self.graph.add_edge(obj_id, datarepr.identifier)

    for obj_class in self.pm.get_extensions('dataconverter.writers'):
      try :
        dataformat = self.fmts[obj_class.dataformat_id]
        datarepr = self.reprs[obj_class.datarepr_id]
      except KeyError :
        print 'Pb loading %s' % obj_class
      else :
        obj = obj_class(dataformat, datarepr)
        obj_id = obj.identifier
        self.writers[obj_id] = obj
        self.graph.add_node(obj_id)
        self.graph.add_edge(datarepr.identifier, obj_id)
        self.graph.add_edge(obj_id, dataformat.identifier)

    for obj_class in self.pm.get_extensions('dataconverter.converters'):
      try :
        dataformats_in = [self.fmts[fmt_id] for fmt_id in obj_class.datafmts_id_in]
        dataformats_out = [self.fmts[fmt_id] for fmt_id in obj_class.datafmts_id_out]
      except KeyError :
        print 'Pb loading %s' % obj_class
      else :
        obj = obj_class(dataformats_in, dataformats_out)
        obj_id = obj.identifier
        self.converters[obj_id] = obj
        self.graph.add_node(obj_id)
        for fmt_id in obj_class.datafmts_id_in :
          self.graph.add_edge(fmt_id, obj_id)
        for fmt_id in obj_class.datafmts_id_out :
          self.graph.add_edge(obj_id, fmt_id)

    for obj_class in self.pm.get_extensions('dataconverter.adapters'):
      try :
        datarepr_in = self.reprs[obj_class.datarepr_id_in]
        datarepr_out = self.reprs[obj_class.datarepr_id_out]
      except KeyError:
        print 'Pb loading %s' % obj_class
      else :
        obj = obj_class(datarepr_in, datarepr_out)
        obj_id = obj.identifier
        self.adapters[obj_id] = obj
        self.graph.add_node(obj_id)
        self.graph.add_edge(datarepr_in.identifier, obj_id)
        self.graph.add_edge(obj_id, datarepr_out.identifier)

    self.nodes = {}
    self.nodes.update(self.fmts)
    self.nodes.update(self.readers)
    self.nodes.update(self.reprs)
    self.nodes.update(self.writers)
    self.nodes.update(self.adapters)
    self.nodes.update(self.converters)

    self._createFmtMatrix()

  def _createFmtMatrix(self):
    self.fmt_matrix = {}

    self.fmt_matrix['convert'] = {}
    for mode in ('python', 'external', 'format') :
      self.fmt_matrix['convert'][mode] = self.getConverterInputFormats(mode)

    self.fmt_matrix['create'] = {}
    for mode in ('python', 'external', 'format') :
      self.fmt_matrix['create'][mode] = self.getCreatorOutputFormats(mode)

  def getInputFormats(self, mode, input_mode):
    return self.fmt_matrix[mode][input_mode]

  def getOutputFormats(self, mode, input_mode):
    return self.fmt_matrix[mode][input_mode]

  def displayGraph(self):
#    import matplotlib.pyplot as plt
    labels = self._getPathLabels()
#    pos = networkx.spring_layout(self.graph, iterations=10)
#    networkx.draw_networkx(self.graph, pos, node_size=500, alpha=0.4,
#                           font_size=10, labels=labels)

#    networkx.draw_graphviz(self.graph, node_size=1000, alpha=0.2,
#                           font_size=12, labels=labels)
#    networkx.write_dot(self.graph, 'graph.dot')
#    plt.savefig("graph.png")
#    plt.show()

    print 'graph saved'
#    return

    print 'GROUPES'
    for i, group in enumerate(networkx.connected_components(self.graph.to_undirected())) :
      print "Group %02d: \n  -" % i,
      print '\n  - '.join(group)

    print 'CONVERSIONS'
    for source in self.fmts :
      for target in self.fmts :
        try :
          path = networkx.shortest_path(self.graph, source=source, target=target)
        except networkx.NetworkXNoPath :
          pass
        else :
          print 'Path: %s -> %s' % (source, target)

    print 'ADAPTATIONS'
    for source in self.adapters :
      for target in self.adapters :
        try :
          path = networkx.shortest_path(self.graph, source=source, target=target)
        except networkx.NetworkXNoPath :
          pass
        else :
          print 'Path: %s -> %s' % (source, target)

    print 'ÉCRITURE'
    for source in self.reprs :
      for target in self.fmts :
        try :
          path = networkx.shortest_path(self.graph, source=source, target=target)
        except networkx.NetworkXNoPath :
          pass
        else :
          print 'Path: %s -> %s' % (source, target)

    print 'LECTURE'
    for source in self.fmts :
      for target in self.reprs :
        try :
          path = networkx.shortest_path(self.graph, source=source, target=target)
        except networkx.NetworkXNoPath :
          pass
        else :
          print 'Path: %s -> %s' % (source, target)

if __name__ == '__main__' :
  pass
